package Servidor;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.HashMap;
import java.util.Iterator;
import java.util.LinkedHashSet;
import java.util.Set;

import Cliente.Messages;

public class Servidor {
    
    private static int port = 25000;

    public static void main (String[] args) throws IOException {

        ServerSocket server = null;
        
        try {
            server = new ServerSocket(port);
        } catch (IOException e) {
            System.err.println("Nao foi possivel escutar na porta: " + port);
            System.err.println(e);
            System.exit(1);
        }

        Socket client = null;
        
        while(true) {
            
            try {
                client = server.accept();
            } catch (IOException e) {
                System.err.println("Falha no estabelecimento da conexao.");
                System.err.println(e);
                System.exit(1);
            }

            /* inicializa uma thread para cuidar desse cliente */
            Thread t = new Thread (new ConexaoCliente(client));
            t.start();
        }
    }
}


class Protocolo {
    
    private String nick;
    private ConexaoCliente conexao;

    /* um HashMap mapeando nicks->conexões */
    private static HashMap<String, ConexaoCliente> nicks = 
        new HashMap<String, ConexaoCliente>();

    
    /* Coloca um nick no Map */
    private static boolean add_nick (String nick, ConexaoCliente c) {
        if (nicks.containsKey(nick)) {
            return false;
        } else {
            nicks.put(nick, c);
            return true;
        }
    }

    public Protocolo (ConexaoCliente c) {
        nick = null;
        conexao = c;
    }

    private void log (String msg) {
        System.err.println(msg);
    }

    public boolean isAuthenticated() {
        return ! (nick == null);
    }

    public int countChar (final String s, final char c) {
        final char[] chars = s.toCharArray();
        int count = 0;
        for (int i=0; i<chars.length; i++) {
            
            if (chars[i] == c)
                count++; 
        }
        
        return count;
    }
    
    /**
     * Implementa o protocolo de autenticação.
     * Checa se o nick já está em uso.
     */ 
    private String authenticate(String msg) {
        if( (!msg.contains(" ")) && (!msg.contains("\\")) && 
                (!msg.contains("/")) && (!msg.contains("@")) ) {
            
            String tryNick = msg;
            if (add_nick(tryNick, this.conexao) ) {
                log("Nick " + tryNick + " joined.");
                this.nick = tryNick;
                
                Set<String> keys = nicks.keySet();
                ConexaoCliente c;
                String usersOnline = new String("[");
                
                for(String tempNick : keys){
                	if(!tempNick.equals(tryNick)){
                		c = nicks.get(tempNick);
                        c.sendMsg(tryNick + " entrou na sala.");
                	}
                	usersOnline+=tempNick+";";
                }
                
                return "Seja bem-vindo ao nosso chat, " + tryNick + "!"
                        + " Utilize o caracter @ para se dirigir a uma pessoa"
                        + " ou grupo de pessoas em especial a qualquer momento."
                        + usersOnline + "]";
            } else {
                return Messages.NICK_IN_USE.toString();
            }
        } else {
            return Messages.SPECIFY_NICK.toString();
        }
    }
    
    /**
     * Manda uma mensagem para outro(s) usuário(s).
     * @recipients contém uma lista de usuários que receberão a mensagem
     * caso esse atributo seja nulo, a mensagem é tratada como broadcast
     * @msg contém a mensagem a ser enviada
     * Retorna falso se o(s) nick(s) não existir(em) no HashMap
     */
    private boolean sendMsg(String[] recipients, String msg) {
        
        ConexaoCliente c;
        String errorMsg = new String("");
        
        if (recipients == null) {
            //broadcast
            Set<String> keys = nicks.keySet();
            
            for (Iterator<String> it = keys.iterator(); it.hasNext() ; ) {                
                String tempNick = it.next();
                c = nicks.get(tempNick);
                if(!msg.contains("saiu da sala"))
                	c.sendMsg(nick + ": " + msg);
                else
                	c.sendMsg(msg);
            }
            return true;
        }
        else if (recipients.length == 1) {
            //unicast
            if (nicks.containsKey(recipients[0].substring(1))) {
                c = nicks.get(recipients[0].substring(1));
                c.sendMsg(nick + " fala reservadamente para voce: " + msg);
                c = nicks.get(nick);
                c.sendMsg("voce falou reservadamente para " + recipients[0].substring(1) + ":" + msg);
                return true;
            } 
            else {
            	errorMsg = "A mensagem nao pode ser enviada para " +
            				recipients[0].substring(1) + 
            				" pois este usuario nao foi encontrado no chat.";
            	c = nicks.get(nick);
            	c.sendMsg("ERRO: "+ errorMsg);
                return false;
            }
        }
        else {
        	String recptFail = new String("");
            //multicast
            for (String s : recipients) {
            	String recipient = s.substring(1);
                if (nicks.containsKey(recipient)) {
                    c = nicks.get(recipient);
                    c.sendMsg(nick + ": " + msg);
                } 
                else {
                	recptFail += recipient + " ";
                }
            }
            c = nicks.get(nick);
            c.sendMsg(nick + ": " + msg);
            
            if(!recptFail.equals("")){
            	errorMsg = "A mensagem nao pode ser enviada para " + recptFail +
            				" pois este(s) usuario(s) nao foi(foram) encontrado(s) no chat.";
            	c = nicks.get(nick);
            	c.sendMsg("ERRO: "+ errorMsg);
                return false;
            }
            else {
            	return true;
            }
        }
    }

    /**
     * Trata uma mensagem vinda do cliente
     */
    public String process(String msg) {
        
        Set<String> clientesAlvo = new LinkedHashSet<String>();
        
        if (!isAuthenticated()) 
            return authenticate(msg);
        
        int tam = this.countChar(msg, ' ');
        
        if (msg.contains("saiu da sala")){
        	ConexaoCliente c;
        	c = nicks.get(nick);
        	c.closeConnection();
        	nicks.remove(nick);
        }
        
        String[] msg_parts = new String[tam+1];
        msg_parts = msg.split(" ");
        for (String m : msg_parts) {
            if (m.startsWith("@")) {
                clientesAlvo.add(m);
                msg = msg.replace(m, "").trim();
            }
        }
        
        if (clientesAlvo.isEmpty()) {
            //broadcast
            if (this.sendMsg(null, msg)) {
                return "";
            }
            else {
                return Messages.SEND_FAILED.toString();
            }
        }
        else {
            String[] clientesArray = new String[clientesAlvo.size()];
            clientesArray = (String[]) clientesAlvo.toArray(clientesArray);

            if (this.sendMsg(clientesArray, msg)) {
            	return "";
            }
            else {
                return Messages.SEND_FAILED.toString();
            }
        }

    }
    
}


class ConexaoCliente implements Runnable {
    
    private Socket client;
    private BufferedReader in = null;
    private PrintWriter out = null;

    ConexaoCliente(Socket client) {
        this.client = client;
        try {
            /* cria um canal para enviar dados para esse cliente */
            in = new BufferedReader(new InputStreamReader(
                        client.getInputStream()));
            /* e um canal para receber dados desse cliente */
            out = new PrintWriter(client.getOutputStream(), true);
        } catch (IOException e) {
            System.err.println(e);
            return;
        }
    }

    public void run() {
        String msg, response;
        Protocolo protocol = new Protocolo(this);
        try {
            /* lê mensagens do cliente, checa se elas estão de acordo 
             * com o protocolo e envia uma resposta ao cliente
             */
            while ((msg = in.readLine()) != null) {
                response = protocol.process(msg);
                if (response != null && !response.equals(""))
                	out.println(response);
            }
        } catch (IOException e) {
            System.err.println(e);
        }
    }

    public void sendMsg(String msg) {
        out.println(msg);
    }
    
    public void closeConnection(){
    	try {
			client.close();
		} catch (IOException e) {
			e.printStackTrace();
		}
    }
}
